import { Callout, Tab, Tabs } from 'nextra/components';
import { NextSeo } from 'next-seo';

<NextSeo description="Run Symfony serverless on AWS Lambda using Bref." />

# Serverless Symfony - Getting started

This guide helps you run Symfony applications on AWS Lambda using Bref. These instructions are kept up to date to target the latest Symfony version.

<Callout>
    A demo application is available on GitHub at [github.com/brefphp/examples](https://github.com/brefphp/examples).
</Callout>

## Setup

First, **follow the [Setup guide](../setup.mdx)** to create an AWS account and install the necessary tools.

Next, in an existing Symfony project, install Bref and the [Symfony Bridge package](https://github.com/brefphp/symfony-bridge).

```bash
composer require bref/bref bref/symfony-bridge --update-with-dependencies
```

Next, create a `serverless.yml` configuration file at the root of your project by running:

```bash
vendor/bin/bref init symfony
```

(you can preview that file [here](https://github.com/brefphp/bref/blob/master/template/http/serverless.yml))

You still have a few modifications to do on the application to make it compatible with AWS Lambda.

Since [the filesystem is readonly](/docs/environment/storage.md) except for `/tmp` we need to customize where the cache and logs are stored in the `src/Kernel.php` file. This is automatically done by the bridge, you just need to use the `BrefKernel` class instead of the default `BaseKernel`:

```diff filename="src/Kernel.php"
namespace App;

+ use Bref\SymfonyBridge\BrefKernel;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;

- class Kernel extends BaseKernel
+ class Kernel extends BrefKernel
{
    // ...
```

## Deployment

Let's deploy the application to AWS Lambda:

<Tabs items={['Serverless CLI', 'Bref Cloud']}>
    <Tab>
        ```bash
        serverless deploy
        ```
    </Tab>
    <Tab>
        ```bash
        bref deploy
        ```
    </Tab>
</Tabs>

When finished, the `deploy` command will show the URL of the application.

### Deploying for production

At the moment, we deployed our local codebase to Lambda. When deploying for production, we probably don't want to deploy:

- development dependencies,
- our local `.env` files,
- or any other dev artifact.

Follow [the deployment guide](/docs/deploy.md#deploying-for-production) for more details.

Separately, on cold starts, Symfony will boot with an empty cache directory. It will build the cache on the fly, which can take a few seconds depending on the complexity of the application.

To optimize cold starts, you can deploy the application with a warm cache. In a simple application it means that the deployment script should include `cache:warmup` to look something like this:

<Tabs items={['Serverless CLI', 'Bref Cloud']}>
    <Tab>
        ```bash
        # Install dependencies
        composer install --classmap-authoritative --no-dev --no-scripts

        # Warmup the cache
        bin/console cache:clear --env=prod

        # Disable use of Dotenv component
        echo "<?php return [];" > .env.local.php

        serverless deploy
        ```
    </Tab>
    <Tab>
        ```bash
        # Install dependencies
        composer install --classmap-authoritative --no-dev --no-scripts

        # Warmup the cache
        bin/console cache:clear --env=prod

        # Disable use of Dotenv component
        echo "<?php return [];" > .env.local.php

        bref deploy
        ```
    </Tab>
</Tabs>

#### Optimizing caches

When running Symfony on Lambda you should avoid writing to the filesystem. If you pre-warm the cache before deploying you are mostly fine. But you should also make sure you never write to a filesystem cache like `cache.system` or use a pool like:

```yaml
framework:
    cache:
        pools:
            my_pool:
                adapter: cache.adapter.filesystem
```

If you don't write to such cache pool you can optimize your setup by not copying the `var/cache/pools` directory. The change below will make sure to symlink the `pools` directory.

```diff filename="src/Kernel.php"
class Kernel extends BrefKernel
{
    // ...

+    protected function getWritableCacheDirectories(): array
+    {
+        return [];
+    }
}
```

## Troubleshooting

In case your application is showing a blank page after being deployed, [have a look at the logs](../environment/logs.md).

## Logs

Thanks to the Bref integration, Symfony will automatically log to CloudWatch via `stderr`. You don't have to do anything.

You can learn more about logs in the [Logs guide](../environment/logs.md).

It is recommended you enable Bref's logs formatter optimized for CloudWatch:

```yaml filename="config/packages/prod/monolog.yaml"
monolog:
    handlers:
        file:
            type: stream
            level: info
            formatter: 'Bref\Monolog\CloudWatchFormatter'
```

<Callout>
    This formatter will be enabled by default in Bref v3.
</Callout>

With this formatter, logs will contain structured data that can be filtered in CloudWatch Logs Insights. For example, you can filter by log level or exception class.

## Website assets

Have a look at the [Website guide](../use-cases/websites.mdx) to learn how to deploy a website with assets.

## Symfony Console

As you may have noticed, we define a function named "console" in `serverless.yml`. That function is using the [Console runtime](../runtimes/console.mdx), which lets us run the Symfony Console on AWS Lambda.

For example, to execute an `bin/console` command on Lambda, run the command below:

<Tabs items={['Serverless CLI', 'Bref Cloud']}>
    <Tab>
        ```bash
        serverless bref:cli --args="{console command and options}"
        ```

        For example:

        ```bash
        serverless bref:cli --args="doctrine:migrations:migrate"
        ```
    </Tab>
    <Tab>
        ```bash
        bref command "{console command and options}"
        ```

        For example:

        ```bash
        bref command "doctrine:migrations:migrate"
        ```
    </Tab>
</Tabs>

For more details follow [the "Console" guide](../runtimes/console.mdx).

## Logs

By default, Symfony logs to `stderr`. That is great because Bref [automatically forwards `stderr` to AWS CloudWatch](/docs/environment/logs.md).

However, if your application is using Monolog, you need to configure it to log to `stderr` as well:

```yml filename="config/packages/prod/monolog.yaml" {6}
monolog:
  handlers:
    # ...
    nested:
      type: stream
      path: php://stderr
```

## Trust API Gateway

When hosting a website on Lambda, API Gateway acts as a proxy between the client and your Lambda function.

By default, Symfony doesn't trust proxies for security reasons, but it's safe to do it when using API Gateway and Lambda. This is needed because otherwise, Symfony will not be able to generate URLs properly.

Add the following lines to `config/packages/framework.yaml`:

```yml filename="config/packages/framework.yaml" {2-5}
framework:
  # trust the remote address because API Gateway has no fixed IP or CIDR range that we can target
  trusted_proxies: '127.0.0.1'
  # trust "X-Forwarded-*" headers coming from API Gateway
  trusted_headers: [ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port' ]
```

Note that API Gateway doesn't set the `X-Forwarded-Host` header, so we don't trust it by default. You should only whitelist this header if you set it manually,
for example in your CloudFront configuration (this is done automatically
in [the Cloudfront distribution deployed by Lift](../use-cases/websites.mdx)).

You can get more details in the [Symfony documentation](https://symfony.com/doc/current/deployment/proxies.html).

<Callout>
    Be careful with these settings if your app is also executed outside a Lambda environment (for example on a public server).
</Callout>

### Getting the user IP

**When using CloudFront** on top of API Gateway, you will not be able to retrieve the client IP address, and you will instead get one of CloudFront's IP when
calling `Request::getClientIp()`. If you really need this, you will need to
whitelist [every CloudFront IP](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/LocationsOfEdgeServers.html)
in `trusted_proxies`.

## The `kernel.terminate` event

The [`kernel.terminate` event](https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-kernel-terminate) runs **synchronously** on Lambda.

That means that if you use this event, its listeners will be executed **before** the Lambda function returns its response. That will add latency to your response.

To run asynchronous tasks, use the [Symfony Messenger](./messenger.mdx) instead.
